package com.nulldev.util.net.packets;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import javax.swing.JOptionPane;

import com.nulldev.util.crypto.encryptAPI.litestring.LiteString;
import com.nulldev.util.encoding.compression.smartCompression;
import com.nulldev.util.io.MD5Utils;

//import org.nullOpen.openChat.packets.AssignedUsername;
//import org.nullOpen.openChat.packets.ConnectMessage;
//import org.nullOpen.openChat.packets.ConnectPacket;
//import org.nullOpen.openChat.packets.MOTDPacket;
//import org.nullOpen.openChat.packets.MessagePacket;
//import org.nullOpen.openChat.packets.UserList;
//import org.nullOpen.openChat.packets.UserMessagePacket;
//import org.nullOpen.openChat.packets.VoicePacket;
//import org.nullOpen.openChat.packets.VoicePacketLooped;
//import org.nullOpen.openChat.packets.client.ChannelChangeDisallowed;
//import org.nullOpen.openChat.packets.client.ChannelChanged;
//import org.nullOpen.openChat.packets.client.ChannelData;
//import org.nullOpen.openChat.packets.client.RequestChannelChange;
//import org.nullOpen.openChat.packets.sync.BroadcastDataUpdate;
//import org.nullOpen.openChat.packets.sync.ChannelDataSync;
//import org.nullOpen.openChat.packets.sync.GameSync;

import com.nulldev.util.java.JVMUtil;
import com.nulldev.util.java.ReflectionUtils;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.napi.APICall;
import com.nulldev.util.napi.APICommand;
import com.nulldev.util.napi.nullAPI;
import com.nulldev.util.net.tcp.TCPCore;
import com.nulldev.util.net.tcp.client.TCPClient;
import com.nulldev.util.net.tcp.server.TCPServer;
import com.nulldev.util.other.ArrayUtils;
import com.nulldev.util.other.ByteUtils;
import com.nulldev.util.string.StringsUtil;

public class PacketManager{

	/**
	 * nPacket System for Networking <br>
	 * v1.0 - Inital release, added in nullUtil 1.0. <br>
	 * v2.0 - Added in nullUtil 2.0.1 <br>
	 * v2.01 - Fixed a parsing issue in STANDARD_V1. <br>
	 * v2.1 - Updated in nullUtil 2.6 <br>
	 * 	- Added support for __p2c packets a.k.a Compression. <br>
	 *  - Added experimental large packet support. <br>
	 *  	- Compressed packets cannot be split. <br>
	 *  	- Large packets are now split into __part packets, an example is below. <br>
	 *  		- __part[000000000000000000000]data{xEF}]}end [End of parts, can be parsed] <br>
	 *  		- __part[000000000000000000000]data{xEF}]}ep [End of current part] <br>
	 *  v3 - Added NetPacket v2 support. - Updated in nullUtil 2.6 <br>
	 *  - Added NPCrypt for RSA packet encryption. <br>
	 *  - New Packet Format: <br>
	 *  [NP3{NET_PACKET,COMP,REPLY_ID}]DATA <br>
	 *  __part packets are now marked as no longer used in fTCP. <br>
	 *  v3.1 - Redesigned packet system, now based around byte[] not String. <br>
	 *  - Removed 64KB limit on packets, applies to fTCP ONLY, new limit is around ~2.002 GB. <br>
	 *  v3.101 - Added base code to allow up to Long.MAX_VALUE B of data to be transfered. <br>
	 *  TODO: Add LongInputStream <br>
	 *  TODO: Add Hack to allow byte arrays larger than ~2GB <br>
	 *  - fUDP now supports packets larger than 64KB, up to 2GB. <br>
	 *  v3.102 - Added FULL packet encryption support [ID + METADATA + DATA] <br>
	 *  - Requires nCrypt to work! (call createKeyPair() or createKeyPair4096()) <br>
	 *  - FULL packet encryption example: <br>
	 *  	- [NP3E]*data* <br>
	 *  	or <br>
	 *  	- [NP3E]000000000000E== <br>
	 *  	or <br>
	 *  	- [NP3E]0000000000EEE== <br>
	 *  	&nbsp;&nbsp;AAAAAAAAAAAAAAAA==
	 *  v3.11 - Added packet encryption for servers. <br>
	 *  Current nCrypt Status: <br>
	 *  [ Client ] ========= ENCRYPTED ============> [ Server ] <br>
	 *  [ Client ] <======== ENCRYPTED ============= [ Server ] <br>
	 *  - Packets can be fully ENCRYPTED both ways. <br>
	 *  - Added NP3 Compression (Currently experimental) <br>
	 *  - Added DSAP support to TCP/UDP/rUDP. <br>
	 *  - Added DSAP. (Dynamic Socket Allocation Protocol).
	 *  v3.11.1: <br>
	 *  - Added readTime metadata for speed testing.
	 */
	public static final float NPACKET_SYSTEM_VERSION = 3.111f;
	
	private static final LinkedList<APICommand> commands = new LinkedList<APICommand>();
	private static final LinkedList<NetPacket> PACKETS = new LinkedList<NetPacket>();
	private static final Logger log = LoggerFactory.getLoggerName("PacketManager", true, false);
	
	public static final NetPacket GENERIC = new NetPacket("Default Packet", "00", "", PacketType.UNIVERSAL) {
		
		@Override
		public void server(byte[] data, byte[] raw, final NetInfo connection) {
			if (data.length < 25) {
				log.warn("Tried to call server() in GENERIC packet. [Data: " + createString(data) + "]");
			}else {
				log.warn("Tried to call server() in GENERIC packet. [Data: " + MD5Utils.getMD5Hex(data) + "]");
			}
		}
		
		@Override
		public void client(byte[] data, byte[] raw, final NetInfo client) {
			if (data.length < 25) {
				log.warn("Tried to call client() in GENERIC packet. [Data: " + createString(data) + "]");
			}else {
				log.warn("Tried to call client() in GENERIC packet. [Data: " + MD5Utils.getMD5Hex(data) + "]");
			}
		}
	};
	
	public static final NetPacket PARTIAL = new NetPacket("Partial Packet", "01", "", PacketType.UNIVERSAL) {
		
		@Override
		public void server(byte[] data, byte[] raw, final NetInfo connection) {
			if (raw.length < 25) {
				log.warn("Tried to call server() in PARTIAL packet. [Data: " + createString(raw) + "]");
			}else {
				log.warn("Tried to call server() in PARTIAL packet. [Data: " + StringsUtil.hash(raw) + "]");
			}
		}
		
		@Override
		public void client(byte[] data, byte[] raw, final NetInfo client) {
			if (raw.length < 25) {
				log.warn("Tried to call client() in PARTIAL packet. [Data: " + createString(raw) + "]");
			}else {
				log.warn("Tried to call client() in PARTIAL packet. [Data: " + StringsUtil.hash(raw) + "]");
			}
		}
	};
	
	private static boolean initd;
	
	static {
		if (!initd) {
			commands.add(new APICommand("api.isSupported", new APICall<Boolean>() {
				
				@Override
				public Boolean called(final Object... data) {
					return true;
				}
			}));
			// ADD YOUR PACKETS HERE
			try {
//				final Class[] xyz = new Class[] {PacketAPILevel.class, PingPacket.class, PongPacket.class, RemovedByServer.class, SDTPSupported.class, UnsupportedOpeartion.class,
//						AssignedUsername.class, ConnectPacket.class, ConnectMessage.class, MessagePacket.class, UserList.class, UserMessagePacket.class, VoicePacket.class, VoicePacketLooped.class,
//						P2PCDefineIP.class, P2PCKey.class, P2PCRequestKey.class, ChannelChangeDisallowed.class, ChannelData.class, ChannelChanged.class, RequestChannelChange.class, MOTDPacket.class,
//						ChannelDataSync.class, GameSync.class, BroadcastDataUpdate.class
//					};
				final ArrayList<Class> classes = new ArrayList<Class>();
				try {
					classes.addAll(ReflectionUtils.ClassUtil.getClasses("com.nulldev.util.net.packets.generic", "com.nulldev.util.net.packets.p2pc", 
							"com.nulldev.util.net.packets.sdtp.html_server", "com.nulldev.util.net.packets.sdtp.html_proxy", 
							"com.nulldev.util.net.packets.sdtp.fileServer", "com.nulldev.util.net.packets.sdtp.ipLookup",
							"com.nulldev.util.net.packets.npcrypt", 
							"com.nulldev.util.net.qclp.packets", "com.nulldev.util.net.dsap.packets",
							"com.nulldev.util.net.protocols.openSDTP2.transferTest")); 
//					if (classes.size() < MIN_EXPECTED) {
//						throw new Exception("Size mismatch with result and expected! [Go]");
//					}else {
//						log.info("init().packetLoader() -> Found " + (classes.size() - 10) + " packets, expected a minimum of " + MIN_EXPECTED + " packets.");
//					}
				}catch (Exception e) {
					log.error("Hit an error using to automated loading method, falling back to the old one...", e);
				}
				/**
				final Class[] classes = new Class[] {PacketAPILevel.class, PingPacket.class, PongPacket.class, RemovedByServer.class, SDTPSupported.class, UnsupportedOpeartion.class,
						AssignedUsername.class, ConnectPacket.class, ConnectMessage.class, MessagePacket.class, UserList.class, UserMessagePacket.class, VoicePacket.class, VoicePacketLooped.class,
						P2PCDefineIP.class, P2PCKey.class, P2PCRequestKey.class
				};*/
				final Class<NetPacket> COMPARE_TO = NetPacket.class;
				for (final Class c : classes) {
					if (JVMUtil.isExtended(c, NetPacket.class)) {
						/*
						 * Fixed
						 */
//						if (!ArrayUtils.contains(xyz, c)) {
//							log.warn("Class '" + c.getName() + "' is not in the variable 'xyz' this will mean it will not be loaded when a JAR file is made.");
//						}
						final NetPacket casted = (NetPacket) c.newInstance();
						if (casted.getAPILevel() == -1) {
							log.warn("Class '" + c.getName() + "' is missing it's API level. This must be fixed as this packet will not be added.");
						}
						PACKETS.add(casted);
					}
				}
			}catch (Exception e) {
				e.printStackTrace();
				JOptionPane.showMessageDialog(null, "Failed to load PacketManager!" + 
						JVMUtil.lineSep() + "Error log:" + JVMUtil.lineSep()
						+ JVMUtil.fromError(e), "nullUtil: Error", JOptionPane.ERROR_MESSAGE);
				JVMUtil.exit();
			}
			
			
			initd = true;
		}
	}
	
	public static void registerPackets(final NetPacket... packets) {
		for (final NetPacket np : packets) {
			if (np != null) {
				if (!PACKETS.contains(np)) {
					PACKETS.add(np);
				}
			}
		}
	}
	
	public static void registerPackets(final String... packets) {
		final ArrayList<Class> classes = new ArrayList<Class>();
		try {
			classes.addAll(ReflectionUtils.ClassUtil.getClasses(packets)); 
		}catch (Exception e) {
			log.error("registerPackets() -> Hit an error using to automated loading method, falling back to the old one...", e);
		}
		final Class<NetPacket> COMPARE_TO = NetPacket.class;
		int fnd = 0;
		for (final Class c : classes) {
			try {
				if (JVMUtil.isExtended(c, NetPacket.class)) {
					final NetPacket casted = (NetPacket) c.newInstance();
					if (casted.getAPILevel() == -1) {
						log.warn("registerPackets() -> Class '" + c.getName() + "' is missing it's API level. This must be fixed as this packet won't be registered.");
					}else if (!PACKETS.contains(casted)) {
						PACKETS.add(casted);
						fnd += 1;
					}else {
						log.error("registerPackets() -> Duplicate packet found! Not loading.");
					}
				}
			}catch (Exception e) {
				log.error("registerPackets() -> Hit an error with loading a packet. [class=" + c.getName() + "]", e);
			}
		}
		log.info("init().packetLoader() -> Found " + (fnd) + " packets.");
	}
	
	/**
	 * Calls static method
	 */
	public static void init() {
	}
	
	public static NetPacket byID(final String id) {
		for (final NetPacket packet : PACKETS) {
			if (packet.getID().equals(id)) {
				return packet;
			}
		}
		return GENERIC;
	}
	
	public static NetPacket byID(final String id, final PacketType type) {
		final LinkedList<NetPacket> packets = (LinkedList<NetPacket>) PACKETS.clone();
		for (final NetPacket packet : packets) {
			if (packet.getID().equals(id) && packet.type() == type) {
				return packet;
			}
		}
		return GENERIC;
	}
	
	public static NetPacket fromID(final String id, final String data) {
		return fromID(id, data.getBytes());
	}

	public static NetPacket fromID(final String id, final byte[] data) {
		final LinkedList<NetPacket> packets = (LinkedList<NetPacket>) PACKETS.clone();
		for (final NetPacket packet : packets) {
			if (packet.getID().equals(id)) {
				final NetPacket output = (NetPacket) packet.clone();
				output.changeData(data);
				return output;
			}
		}
		final NetPacket g = (NetPacket) GENERIC.clone();
		g.changeData(data);
		return g;
	}
	
	/**
	 * Partial Packet store, removed once fully recieved. <br>
	 * Format: Packet.DOWNLOAD_ID, List(Packet.Current_Data)
	 */
	@Deprecated
	private static final HashMap<String, LinkedList<String>> PARTIAL_PACKET_DATA
										= new HashMap<String, LinkedList<String>>();

	/**
	 * Parses the packet recieved 
	 * @param callback [Object called from, expected "this"]
	 * @param callback1 [Object only for servers, can be null]
	 * @param data [Data]
	 * @returns NetPacket
	 */
	public static NetPacket parsePacket(final Object callback, final Object callback1, final String data) {
		return parsePacket(callback, callback1, data, new byte[0], 0);
	}
	
	/**
	 * Parses the packet recieved 
	 * @param callback [Object called from, expected "this"]
	 * @param callback1 [Object only for servers, can be null]
	 * @param data [Data]
	 * @param extData [Extra data, bytes, used by fTCP]
	 * @returns NetPacket
	 */
	public static NetPacket parsePacket(final Object callback, final Object callback1, final String data,
			final byte[] extData) {
		return parsePacket(callback, callback1, data, extData, 0);
	}
	
	/**
	 * Parses the packet recieved 
	 * @param callback [Object called from, expected "this"]
	 * @param callback1 [Object only for servers, can be null]
	 * @param data [Data]
	 * @param extData [Extra data, bytes, used by fTCP]
	 * @returns NetPacket
	 */
	public static NetPacket parsePacket(final Object callback, final Object callback1, final String data,
			final byte[] extData, final long readTime) {
		if (!data.startsWith("__p2") && !data.startsWith("__part") && !data.startsWith("[NP3{") && !data.startsWith("[NP3E]")) {
			return GENERIC;
		}
		if (data.startsWith("__part")) {
			//log.binfo("parsePacket(callback, data) -> partial packet detected, caching for now...");
			final String id = StringsUtil.substringBetween(data, "__part[", "]");
			final boolean finalPart = data.endsWith("}]}end");
			final String filter = "__part[" + id + "]";
			final String from = StringsUtil.substringBetween(data.substring(data.indexOf(filter) + filter.length()), "data{", "}]}e"); 
			if (PARTIAL_PACKET_DATA.containsKey(id)) {
				final LinkedList<String> currentData = PARTIAL_PACKET_DATA.get(id);
				currentData.add(from);
				PARTIAL_PACKET_DATA.put(id, currentData);
			}else {
				final LinkedList<String> currentData = new LinkedList<String>();
				currentData.add(from);
				PARTIAL_PACKET_DATA.put(id, currentData);
			}
			if (finalPart) {
				final LinkedList<String> packetData0 = PARTIAL_PACKET_DATA.get(id);
				String packetData = ArrayUtils.StringArrayUtils.toString(ArrayUtils.linkedListToArray(packetData0));
				if (packetData.startsWith("data{") && packetData.endsWith("}")) {
					final String f = "data{";
					packetData = packetData.substring(f.length(), packetData.length() - 1);
				}else if (packetData.startsWith("data{")) {
					final String f = "data{";
					packetData = packetData.substring(f.length(), packetData.length());
				}
				if (callback != null) {
					PARTIAL_PACKET_DATA.remove(id);
				}
				if (callback instanceof TCPClient) {
					((TCPClient)callback).receive(packetData, packetData, new byte[0], packetData.length(), readTime);
				}else if (callback instanceof TCPServer && callback1 instanceof TCPCore) {
					((TCPServer)callback).receive((TCPCore) callback1, packetData, packetData, new byte[0], packetData.length(), readTime);
				}
//				}else if (callback instanceof NEEMServer && callback1 instanceof Node) {
//					((NEEMServer)callback).receive(packetData, (Node) callback1);
//				}else if (callback instanceof NEEMClient && callback1 instanceof Node) {
//					((NEEMClient)callback).receive(packetData, (Node) callback1);
//				}
			}
			return PARTIAL;
			//UNSUPPORTED DECRYPTION
		}else if (data.startsWith("[NP3E]")) {
			if (callback instanceof TCPClient) {
				if (extData == null || extData.length == 0) {
					// WARNING: This is unsupported, preferBytePayload(true) should be enforced.
					// WARNING: NOT CONFIRMED WORKING
					final String dta = data.substring(5);
					/*
					 * Base64 --> Raw String + Data in Base64 
					 * 				|			|
					 * 				|			/
					 * 				|	/-------
					 * 				V	V
					 * 			 	parse();
					 */
					final byte[] dec = ((TCPClient)callback).decrypt(ByteUtils.fromBase64(dta));
					JVMUtil.async_println(new String(dec));
					JVMUtil.async_println("UNSUPPORTED DECRYPTION");
					return parsePacket(callback, callback1, new String(dec));
				}else {
					/*
					 * HEADER contains Packet Header
					 * BYTE_DATA has encrypted packet data 
					 */
					final String dta0 = data.substring(5);
					/* Decrypted Packet Header */
					final String dta = ((TCPClient)callback).decrypt(dta0);
					/* Decrypted Packet Data */
					final byte[] dta_b = ((TCPClient)callback).decrypt(extData);
					return parsePacket(callback, callback1, dta, dta_b, readTime);
				}
			}else if (callback instanceof TCPServer) {
				if (extData == null || extData.length == 0) {
					// WARNING: This is unsupported, preferBytePayload(true) should be enforced.
					// WARNING: NOT CONFIRMED WORKING
					final String dta = data.substring(5);
					/*
					 * Base64 --> Raw String + Data in Base64 
					 * 				|			|
					 * 				|			/
					 * 				|	/-------
					 * 				V	V
					 * 			 	parse();
					 */
					final byte[] dec = ((TCPServer)callback).decrypt(ByteUtils.fromBase64(dta));
					JVMUtil.async_println(new String(dec));
					JVMUtil.async_println("UNSUPPORTED DECRYPTION");
					return parsePacket(callback, callback1, new String(dec));
				}else {
					/*
					 * HEADER contains Packet Header
					 * BYTE_DATA has encrypted packet data 
					 */
					final String dta0 = data.substring(5);
					/* Decrypted Packet Header */
					final String dta = ((TCPServer)callback).decrypt(dta0);
					/* Decrypted Packet Data */
					final byte[] dta_b = ((TCPServer)callback).decrypt(extData);
					return parsePacket(callback, callback1, dta, dta_b, readTime);
				}
			}else {
				log.error("Cannot decrypt packet. Returning GENERIC...");
				return GENERIC;
			}
		}else if (data.startsWith("[NP3{")) {
			// PACKET MODEL
			// [NP3{PACKET_NAME,COMP,ENC_NCRYPT}]
			// See PacketMode.STANDARD_V2
			final String np3_decl = StringsUtil.substringBetween(data, "[NP3{", "}]");
			final String[] NP3_ARGS = np3_decl.split(",");
			final String PACKET_NAME = NP3_ARGS[0];
			final boolean compression = ArrayUtils.StringArrayUtils.contains(NP3_ARGS, "COMP");
			final boolean enc_ncrypt = ArrayUtils.StringArrayUtils.contains(NP3_ARGS, "ENC_NCRYPT");
			final boolean compression_smart = ArrayUtils.StringArrayUtils.contains(NP3_ARGS, "COMP_SC");
			//JVMUtil.async_println("Compressed Packet: " + compression_smart);
			final String finData0 = "[NP3{" + np3_decl + "}]";
			final int iof = data.indexOf(finData0);
			String finData = data.substring(finData0.length());
			//JVMUtil.async_println("data: " + data);
			//JVMUtil.async_println("finD:" + data);
			if (extData == null || extData.length == 0) {
				// data present, no byte reading required.
				//System.out.println("pre_dec: " + finData);
				if (compression) {
					finData = LiteString.unzipBase64(finData);
				}else if (enc_ncrypt && callback instanceof TCPClient) {
					//System.out.println("Decrypting...");
					final TCPClient cback = (TCPClient) callback;
					final String decryptedData = cback.decrypt(finData);
					//System.out.println("Decrypting...Result: " + decryptedData);
					finData = decryptedData;
				}else if (compression_smart) {
					finData = null;
				}
				return fromID(PACKET_NAME, finData);
			}else {
				//System.out.println("Downloading extra data..." + extData.length + " bytes");
				//finData = new String(extData);
				byte[] finDataB = extData;
				//System.out.println("pre_dec: " + new String(finDataB));
				if (compression_smart) {
					finDataB = null; //FIXME: smartCompression.decompress(ByteUtils.fromBase64(new String(finDataB)))
				}else if (compression) {
					finDataB = LiteString.unzipB(finDataB);
				}else if (enc_ncrypt && callback instanceof TCPClient) {
					//System.out.println("Decrypting...");
					final TCPClient cback = (TCPClient) callback;
					final byte[] decryptedData = cback.decrypt(finDataB);
					//System.out.println("Decrypting...Result: " + decryptedData);
					finDataB = decryptedData;
				}
				return fromID(PACKET_NAME, finDataB);
			}
		}else if (data.startsWith("__p2c[")) {
			final String id = StringsUtil.substringBetween(data, "__p2c[", "]");
			final String filter = "__p2c[" + id + "]";
			final String from = data.substring(data.indexOf(filter) + filter.length());
			return fromID(id, LiteString.unzipBase64(from));
		}else if (data.startsWith("__p2[")) {
			final String id = StringsUtil.substringBetween(data, "__p2[", "]");
			final String filter = "__p2[" + id + "]";
			final String from = data.substring(data.indexOf(filter) + filter.length());
			return fromID(id, from);
		}else {
			JVMUtil.async_error("FAILED TO READ PACKET");
			JVMUtil.async_error(data);
			return GENERIC;
		}
	}

	public static NetPacket byName(final String name) {
		return byID(name);
//		final LinkedList<NetPacket> packets = (LinkedList<NetPacket>) PACKETS.clone();
//		for (final NetPacket packet : packets) {
//			if (packet.getPacketName().equals(name)) {
//				return packet;
//			}
//		}
//		return GENERIC;
	}

	public static LinkedList<NetPacket> get() {
		return PACKETS;
	}

	public static LinkedList<NetPacket> getClone() {
		return (LinkedList<NetPacket>) PACKETS.clone();
	}

	public static nullAPI getAPICalls() {
		final nullAPI calls = new nullAPI();
		
		calls.registerAll(commands);
		
		return calls;
	}

	public static void registerCalls(final LinkedList<APICommand> commands) {
		PacketManager.commands.addAll(commands);
	}

	public static void register(final nullAPI api) {
		final LinkedList<APICommand> cmds = api.cloneList();
		registerCalls(cmds);
	}
}
